import SPI;
import Credential;
import Util;
import OpenApiUtil;
import String;
import Map;
import Array;
import EncodeUtil;
import SignatureUtil;
import SLS_Util;

extends SPI;

type @respBodyDecompressType = map[string][string];
type @reqBodyCompressType = map[string][string];

init(){ 
  super();
  @respBodyDecompressType = {
    PullLogs = ['zstd', 'lz4', 'gzip'],
    GetLogsV2 = ['zstd', 'lz4', 'gzip'],
    PreviewSPL = ['lz4']
  };
  @reqBodyCompressType = {
    PutLogs = ['zstd', 'lz4', 'deflate'],
    PreviewSPL = ['lz4']
  };
}

async function modifyConfiguration(context: SPI.InterceptorContext, attributeMap: SPI.AttributeMap): void {
  var config = context.configuration;
  config.endpoint = getEndpoint(config.regionId, config.network, config.endpoint);
}

async function modifyRequest(context: SPI.InterceptorContext, attributeMap: SPI.AttributeMap): void {
  var request = context.request;
  var hostMap : map[string]string = {};
  if (!Util.isUnset(request.hostMap)) {
    hostMap = request.hostMap;
  }
  var project = hostMap.project;
  var config = context.configuration;
  var credential : Credential = request.credential;
  var credentialModel = credential.getCredential();
  var accessKeyId = credentialModel.accessKeyId;
  var accessKeySecret = credentialModel.accessKeySecret;
  var securityToken = credentialModel.securityToken;
  if (!Util.empty(securityToken)) {
      request.headers['x-acs-security-token'] = securityToken;
  }
  var signatureVersion = Util.defaultString(request.signatureVersion, 'v1');

  var finalCompressType = getFinalRequestCompressType(request.action, request.headers);

  var contentHash = '';

  // get body bytes
  var bodyBytes : bytes = null;
  if (!Util.isUnset(request.body)) {
    if (String.equals(request.reqBodyType, 'json') || String.equals(request.reqBodyType, 'formData')) {
      request.headers['content-type'] = 'application/json';
      var bodyStr = Util.toJSONString(request.body);
      bodyBytes = Util.toBytes(bodyStr);
    } else if (String.equals(request.reqBodyType, 'binary')) {
      // content-type: application/octet-stream
      bodyBytes = Util.assertAsBytes(request.body);
    }
  }

  // get body raw size
  var bodyRawSize : string = '0';
  var rawSizeRef = request.headers['x-log-bodyrawsize']; // for php bug, Argument #1 ($value) could not be passed by reference
  if (!Util.isUnset(rawSizeRef)) {
      bodyRawSize = rawSizeRef;
  } else if (!Util.isUnset(request.body)) {
    bodyRawSize = `${SLS_Util.bytesLength(bodyBytes)}`;
  }

  // compress if needed, and set body and hash
  if (!Util.isUnset(request.body)) {
    if (!Util.empty(finalCompressType)) {
      var compressed = SLS_Util.compress(bodyBytes, finalCompressType);
      bodyBytes = compressed;
    } 
    contentHash = makeContentHash(bodyBytes, signatureVersion);
    request.stream = bodyBytes;
  }

  var host = getHost(config.network, project, config.endpoint);
  request.headers = {
    accept = 'application/json',
    host = host,
    user-agent = request.userAgent,
    x-log-apiversion = '0.6.0',
    ...request.headers
  };
  request.headers['x-log-bodyrawsize'] = bodyRawSize;
  setDefaultAcceptEncoding(request.action, request.headers);

  buildRequest(context); // move param in path to query

  if (String.equals(signatureVersion, 'v4')) {
    if (Util.empty(contentHash)) {
      contentHash = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
    }
    var date = getDateISO8601();
    request.headers['x-log-date'] = date;
    request.headers['x-log-content-sha256'] = contentHash;
    request.headers['authorization'] = getAuthorizationV4(context, date, contentHash, accessKeyId, accessKeySecret);
    return;
  }
  if (!Util.empty(accessKeyId)) {
      request.headers['x-log-signaturemethod'] = 'hmac-sha256';
  }
  request.headers['date'] = Util.getDateUTCString();
  request.headers['content-md5'] = contentHash;
  request.headers['authorization'] = getAuthorization(request.pathname, request.method, request.query, request.headers, accessKeyId, accessKeySecret);
}

async function getFinalRequestCompressType(action: string, headers: map[string]string): string {
  var compressType = headers['x-log-compresstype'];
  var rawSize = headers['x-log-bodyrawsize']; // for php bug, Argument #1 ($value) could not be passed by reference
  // 1. already compressed, has x-log-compresstype and x-log-bodyrawsize in header, we dont need compress here
  if (!Util.isUnset(compressType) && !Util.isUnset(rawSize)) {
    return '';
  }

  // 2. not compressed, but has x-log-compresstype in header, we need compress here
  if (!Util.isUnset(compressType)) {
    return compressType;
  }

  // 3. not compressed, in pre-defined api list, try use default supported compress type in order
  var encodings = @reqBodyCompressType[action];
  if (!Util.isUnset(encodings)) {
    for (var encoding : encodings) {
      if (SLS_Util.isCompressorAvailable(encoding)) {
        headers['x-log-compresstype'] = encoding; // set header x-log-compresstype
        return encoding;
      }
    }
  }

  // 4. otherwise we dont need compress here
  return '';
}


async function setDefaultAcceptEncoding(action: string, headers: map[string]string): void {
  var acceptEncoding = headers['Accept-Encoding']; // for php warning, dont rm this line
  if (!Util.isUnset(acceptEncoding)) {
    return;
  }
  var encodings = @respBodyDecompressType[action];
  if (!Util.isUnset(encodings)) {
    for (var c : encodings) {
      if (SLS_Util.isDecompressorAvailable(c)) {
        headers['Accept-Encoding'] = c;
        return;
      }
    }
  }
}

async function makeContentHash(content: bytes, signatureVersion: string) : string {
  if (String.equals(signatureVersion, 'v4')) {
    if (Util.isUnset(content) || Util.equalString(`${SLS_Util.bytesLength(content)}`, "0")) {
      return 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
    }
    return String.toLower(EncodeUtil.hexEncode(EncodeUtil.hash(content, 'SLS4-HMAC-SHA256')));
  }
  return String.toUpper(EncodeUtil.hexEncode(SignatureUtil.MD5SignForBytes(content)));
}

async function modifyResponse(context: SPI.InterceptorContext, attributeMap: SPI.AttributeMap): void {
  var request = context.request;
  var response = context.response;
  if (Util.is4xx(response.statusCode) || Util.is5xx(response.statusCode)) {
    var error = Util.readAsJSON(response.body);
    var resMap = Util.assertAsMap(error);
    throw {
      code = resMap['errorCode'],
      message = resMap['errorMessage'],
      accessDeniedDetail = resMap['accessDeniedDetail'],
      data = {
        httpCode = response.statusCode,
        requestId = response.headers['x-log-requestid'],
        statusCode = response.statusCode,
      }
    };
  }
  if (!Util.isUnset(response.body)) {
    var bodyrawSize = response.headers['x-log-bodyrawsize'];
    var compressType = response.headers['x-log-compresstype'];
    var uncompressedData : readable = response.body;
    if (!Util.isUnset(bodyrawSize) && !Util.isUnset(compressType)) {
      uncompressedData = SLS_Util.readAndUncompressBlock(response.body, compressType, bodyrawSize);
    }
    if (Util.equalString(request.bodyType, 'binary')) {
      response.deserializedBody = uncompressedData;
    } else if (Util.equalString(request.bodyType, 'byte')) {
      var byt = Util.readAsBytes(uncompressedData);
      response.deserializedBody = byt;
    } else if (Util.equalString(request.bodyType, 'string')) {
      response.deserializedBody = Util.readAsString(uncompressedData);
    } else if (Util.equalString(request.bodyType, 'json')) {
      var obj = Util.readAsJSON(uncompressedData);
      // var res = Util.assertAsMap(obj);
      response.deserializedBody = obj;
    } else if (Util.equalString(request.bodyType, 'array')) {
      response.deserializedBody = Util.readAsJSON(uncompressedData);
    } else {
      response.deserializedBody = Util.readAsString(uncompressedData);
    }
  }
}

async function getEndpoint(regionId: string, network: string, endpoint: string) : string{
  if (!Util.empty(endpoint)) {
    return endpoint;
  }
  if (Util.empty(regionId)) {
    regionId = 'cn-hangzhou';
  }
  if (!Util.empty(network)) {
    if (String.equals(network, 'intranet')) {
      return `${regionId}-intranet.log.aliyuncs.com`;
    } else if (String.equals(network, 'accelerate')) {
      return `log-global.aliyuncs.com`;
    } else if (String.equals(network, 'share')) {
      if (String.equals(regionId, 'cn-hangzhou-corp') || String.equals(regionId, 'cn-shanghai-corp')) {
        return `${regionId}.sls.aliyuncs.com`;
      } else if (String.equals(regionId, 'cn-zhangbei-corp')) {
        return `zhangbei-corp-share.log.aliyuncs.com`;
      }
      return `${regionId}-share.log.aliyuncs.com`;
    }
  }
  return `${regionId}.log.aliyuncs.com`;
}

async function getHost(network: string, project: string, endpoint: string): string {
  if (Util.isUnset(project)) {
    return endpoint;
  }
  return `${project}.${endpoint}`;
}

async function getAuthorization(pathname: string, method: string, query: map[string]string, headers: map[string]string, ak: string, secret: string): string {
  var sign = getSignature(pathname, method, query, headers, secret);
  return `LOG ${ak}:${sign}`;
}

async function getSignature(pathname: string, method: string, query: map[string]string, headers: map[string]string, secret: string): string {
  var resource : string = pathname;
  var stringToSign : string = '';
  var canonicalizedResource = buildCanonicalizedResource(resource, query);
  var canonicalizedHeaders = buildCanonicalizedHeaders(headers);
  stringToSign = `${method}\n${canonicalizedHeaders}${canonicalizedResource}`;
  return EncodeUtil.base64EncodeToString(SignatureUtil.HmacSHA256Sign(stringToSign, secret));
}

async function buildCanonicalizedResource(pathname: string, query: map[string]string): string {
  var canonicalizedResource : string = pathname;
  if (!Util.isUnset(query)) {
    var queryList : [string] = Map.keySet(query);
    var sortedParams = Array.ascSort(queryList);
    var separator : string = '?';
    for(var paramName : sortedParams) {
      canonicalizedResource = `${canonicalizedResource}${separator}${paramName}`;
      var paramValue = query[paramName];
      if (!Util.isUnset(paramValue)) {
        canonicalizedResource = `${canonicalizedResource}=${paramValue}`;
      }
      separator = '&';
    }
  }
  return canonicalizedResource;
}

async function buildCanonicalizedHeaders(headers: map[string]string): string {
  var canonicalizedHeaders : string = '';
  var contentType = headers.content-type;
  if (Util.isUnset(contentType)) {
    contentType = '';
  }
  var contentMd5 = headers.content-md5;
  if (Util.isUnset(contentMd5)) {
    contentMd5 = '';
  }
  canonicalizedHeaders = `${canonicalizedHeaders}${contentMd5}\n${contentType}\n${headers.date}\n`;
  var keys = Map.keySet(headers);
  var sortedHeaders = Array.ascSort(keys);
  for(var header : sortedHeaders) {
    if (String.contains(String.toLower(header), 'x-log-') || String.contains(String.toLower(header), 'x-acs-')) {
      canonicalizedHeaders = `${canonicalizedHeaders}${header}:${headers[header]}\n`;
    }
  }
  return canonicalizedHeaders;
}

async function buildRequest(context: SPI.InterceptorContext): void {
  var request = context.request;
  var resource : string = request.pathname;
  if (!Util.empty(resource)) {
    var paths : [string] = String.split(resource, `?`, 2);
    resource = paths[0];
    if (Util.equalNumber(Array.size(paths), 2)) {
      var params : [string] = String.split(paths[1], '&', null);
      for (var sub : params) {
        var item : [string] = String.split(sub, '=', null);
        var key : string = item[0];
        var value : string = null;
        if (Util.equalNumber(Array.size(item), 2)) {
          value = item[1];
        }
        request.query[key] = value;
      }
    }
  }
  request.pathname = resource;
}

async function getAuthorizationV4(context: SPI.InterceptorContext, date: string, contentHash: string, accessKeyId: string, accessKeySecret: string): string {
  var region = getRegion(context);
  var headerStr = getSignedHeaderStrV4(context.request.headers);

  var dateShort = String.subString(date, 0, 8);
  dateShort = String.replace(dateShort, 'T', '', null); // for fix php sdk bug
  var scope = `${dateShort}/${region}/sls/aliyun_v4_request`;
  var signingkey = getSigningkeyV4('SLS4-HMAC-SHA256', accessKeySecret, region, dateShort);
  var signature = getSignatureV4(context, 'SLS4-HMAC-SHA256', headerStr, date, scope, contentHash, signingkey);
  return `${'SLS4-HMAC-SHA256'} Credential=${accessKeyId}/${scope},Signature=${signature}`;
}

async function getSigningkeyV4(signatureAlgorithm: string, secret: string, region: string, date: string): bytes {
  var sc1 = `aliyun_v4${secret}`;
  var sc2 = SignatureUtil.HmacSHA256Sign(date, sc1);
  var sc3 = SignatureUtil.HmacSHA256SignByBytes(region, sc2);
  var sc4 = SignatureUtil.HmacSHA256SignByBytes('sls', sc3);
  return SignatureUtil.HmacSHA256SignByBytes('aliyun_v4_request', sc4);
}

async function getSignatureV4(context: SPI.InterceptorContext, signatureAlgorithm: string, signedHeaderStr: string, date: string, scope: string, contentSha256: string, signingkey: bytes): string {
  var request = context.request;
  var canonicalURI = '/';
  if (!Util.empty(request.pathname)) {
    canonicalURI = request.pathname;
  }
  var resources = buildCanonicalizedResourceV4(request.query);
  var headers = buildCanonicalizedHeadersV4(request.headers, signedHeaderStr);

  var stringToHash = `${request.method}\n${canonicalURI}\n${resources}\n${headers}\n${signedHeaderStr}\n${contentSha256}`;
  var hex = EncodeUtil.hexEncode(EncodeUtil.hash(Util.toBytes(stringToHash), signatureAlgorithm));

  var stringToSign = `${signatureAlgorithm}\n${date}\n${scope}\n${hex}`;
  var signature = SignatureUtil.HmacSHA256SignByBytes(stringToSign, signingkey);
  return EncodeUtil.hexEncode(signature);
}

async function buildCanonicalizedResourceV4(query: map[string]string): string {
  var canonicalizedResource : string = '';
  if (!Util.isUnset(query)) {
    var queryArray : [string] = Map.keySet(query);
    var sortedQueryArray = Array.ascSort(queryArray);
    var separator : string = '';
    for(var key : sortedQueryArray) {
      canonicalizedResource = `${canonicalizedResource}${separator}${key}`;
      if (!Util.empty(query[key])) {
        canonicalizedResource = `${canonicalizedResource}=${EncodeUtil.percentEncode(query[key])}`;
      }
      separator = '&';
    }
  }
  return canonicalizedResource;
}

async function buildCanonicalizedHeadersV4(headers: map[string]string, signedHeaderStr: string): string {
  var canonicalizedHeaders : string = '';
  var signedHeaders: [string] = String.split(signedHeaderStr, ';', null);
  for (var header : signedHeaders) {
    canonicalizedHeaders = `${canonicalizedHeaders}${header}:${String.trim(headers[header])}\n`;
  }
  return canonicalizedHeaders;
}

async function getSignedHeaderStrV4(headers: map[string]string): string {
  var headersArray : [string] = Map.keySet(headers);
  var sortedHeadersArray = Array.ascSort(headersArray);
  var tmp : string = '';
  var separator : string = '';
  for(var key : sortedHeadersArray) {
    var lowerKey = String.toLower(key);
    if (String.hasPrefix(lowerKey, 'x-log-') || 
        String.hasPrefix(lowerKey, 'x-acs-') ||
        String.equals(lowerKey, 'host') ||
        String.equals(lowerKey, 'content-type')) {
        tmp = `${tmp}${separator}${lowerKey}`;
        separator = ';';
    }
  }
  return tmp;
}

async function getRegion(context: SPI.InterceptorContext): string {
  var config = context.configuration;
  if (!Util.isUnset(config.regionId) && !Util.empty(config.regionId)) {
    return config.regionId;
  }
  // try parse region from endpoint
  // do not use String.subString, subString has bug in php implementation
  var region: string = String.replace(config.endpoint, '.log.aliyuncs.com', '', null);
  region = String.replace(region, '.sls.aliyuncs.com', '', null);
  if (String.equals(region, config.endpoint)) {
      throw {
        code = 'ClientConfigError',
        message = 'The regionId configuration of sls client is missing.'
      };
  }
  region = String.replace(region, '-share', '', null);
  region = String.replace(region, '-intranet', '', null);
  region = String.replace(region, '-vpc', '', null);
  return region;
}

// format: YYYYMMDDTHHMMSSZ
async function getDateISO8601() : string {
  var date = OpenApiUtil.getTimestamp(); // 2024-02-04T11:31:58Z
  date = String.replace(date, '-', '', null);
  return String.replace(date, ':', '', null);
} 